﻿using System;
using System.Linq;
using System.Diagnostics;
using Kaleida.ServiceMonitor.Model.Parsing;
using Kaleida.ServiceMonitor.Model.StructuredSource;
using NUnit.Framework;

namespace Kaleida.UnitTests.Model.Parsing
{
    [TestFixture]
    public class ParserTests
    {
        [Test]
        public void TestEmptyString()
        {
            var script = ParseAndLog("");

            Assert.AreEqual(0, script.Operations.Count);
            Assert.AreEqual(0, script.Comments.Count);
            Assert.AreEqual(0, script.Directives.Count);
        }

        [Test]
        public void TestRequestWithNoArguments()
        {
            var script = ParseAndLog("my-action");

            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action");
            AssertAction(script.Operations[0].HandlerAction, "do-nothing");
            Assert.IsNull(script.Operations[0].Impersonation);
        }

        [Test]
        public void TestRequestWithNoArgumentsAndImpersonation()
        {
            var script = ParseAndLog("my-action runas \"myDomain\\myUser\", \"myPassword\"");

            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action");
            AssertAction(script.Operations[0].HandlerAction, "do-nothing");
            Assert.IsNotNull(script.Operations[0].Impersonation);
            Assert.AreEqual("myDomain", script.Operations[0].Impersonation.Domain);
            Assert.AreEqual("myUser", script.Operations[0].Impersonation.Username);
            Assert.AreEqual("myPassword", script.Operations[0].Impersonation.Password);
        }

        [Test]
        public void TestRequestWithNoArgumentsAndImpersonationWithoutDomain()
        {
            var script = ParseAndLog("my-action runas \"myUser\", \"myPassword\"");

            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action");
            AssertAction(script.Operations[0].HandlerAction, "do-nothing");
            Assert.IsNotNull(script.Operations[0].Impersonation);
            Assert.AreEqual("", script.Operations[0].Impersonation.Domain);
            Assert.AreEqual("myUser", script.Operations[0].Impersonation.Username);
            Assert.AreEqual("myPassword", script.Operations[0].Impersonation.Password);
        }

        [Test]
        public void TestRequestWithOneArgument()
        {
            var script = ParseAndLog("my-action \"arg1\"");

            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action", "arg1");
            AssertAction(script.Operations[0].HandlerAction, "do-nothing");
            Assert.IsNull(script.Operations[0].Impersonation);
        }

        [Test]
        public void TestRequestWithOneArgumentAndImpersonation()
        {
            var script = ParseAndLog("my-action \"arg1\" runas \"myDomain\\myUser\", \"myPassword\"");

            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action", "arg1");
            AssertAction(script.Operations[0].HandlerAction, "do-nothing");
            Assert.IsNotNull(script.Operations[0].Impersonation);
            Assert.AreEqual("myDomain", script.Operations[0].Impersonation.Domain);
            Assert.AreEqual("myUser", script.Operations[0].Impersonation.Username);
            Assert.AreEqual("myPassword", script.Operations[0].Impersonation.Password);
        }

        [Test]
        public void TestArgumentContainingQuotationMarks()
        {
            var script = ParseAndLog("my-action \"\"\"Hello\"\", she said\"");

            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action", "\"Hello\", she said");
        }

        [Test]
        public void TestRequestWithMultipleArgument()
        {
            var script = ParseAndLog("my-action \"arg1\", \"arg2\", \"arg3\"");
            
            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action", "arg1", "arg2", "arg3");
            AssertAction(script.Operations[0].HandlerAction, "do-nothing");
            Assert.IsNull(script.Operations[0].Impersonation);
        }
        
        [Test]
        public void TestRequestWithMultipleArgumentAndImpersonation()
        {
            var script = ParseAndLog("my-action \"arg1\", \"arg2\", \"arg3\" runas \"myDomain\\myUser\", \"myPassword\"");
            
            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action", "arg1", "arg2", "arg3");
            AssertAction(script.Operations[0].HandlerAction, "do-nothing");
            Assert.IsNotNull(script.Operations[0].Impersonation);
            Assert.AreEqual("myDomain", script.Operations[0].Impersonation.Domain);
            Assert.AreEqual("myUser", script.Operations[0].Impersonation.Username);
            Assert.AreEqual("myPassword", script.Operations[0].Impersonation.Password);
        }


        [Test]
        public void TestRequestWithNoArgsAndResponseWithNoArgs()
        {
            var script = ParseAndLog("my-action my-handler");
            
            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action");
            AssertAction(script.Operations[0].HandlerAction, "my-handler");
        }

        [Test]
        public void TestRequestWithNoArgsAndResponseWithOneArg()
        {
            var script = ParseAndLog("my-action my-handler \"a\"");
            
            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action");
            AssertAction(script.Operations[0].HandlerAction, "my-handler", "a");
        }

        [Test]
        public void TestRequestWithNoArgsAndResponseWithMultipleArgs()
        {
            var script = ParseAndLog("my-action my-handler \"a\", \"b\", \"c\"");
            
            Assert.AreEqual(1, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action");
            AssertAction(script.Operations[0].HandlerAction, "my-handler", "a", "b", "c");
        }

        [Test]
        public void TestMultipleCommandsContainingSimpleRequest()
        {
            var script = ParseAndLog("my-action1\r\n" +
                                     "my-action2\r\n");

            Assert.AreEqual(2, script.Operations.Count);
        }

        [Test]
        public void TestDuplicateNewLines()
        {
            var script = ParseAndLog("my-action1\r\n\r\n\r\n" +
                                     "my-action2\r\n\r\n");

            Assert.AreEqual(2, script.Operations.Count);
        }

        [Test]
        public void TestMultipleCommandsWithMixedEndings()
        {
            var script = ParseAndLog("my-action1\r" +
                                     "my-action2\r\n" +
                                     "my-action3\n");

            Assert.AreEqual(3, script.Operations.Count);
            AssertAction(script.Operations[0].RequestAction, "my-action1");
            AssertAction(script.Operations[1].RequestAction, "my-action2");
            AssertAction(script.Operations[2].RequestAction, "my-action3");
        }

        [Test]
        public void TestMultipleCommandsWithFull()
        {
            var script = ParseAndLog("my-action1 \"a\", \"b\", \"c\" my-handler1 \"1\", \"2\", \"3\"\r\n" +
                                     "my-action2 \"p\", \"q\", \"r\" my-handler2 \"4\", \"5\", \"6\"\r\n" +
                                     "my-action3 \"x\", \"y\", \"z\" my-handler3 \"7\", \"8\", \"9\"\r\n");

            Assert.AreEqual(3, script.Operations.Count);
        }

        [Test]
        public void TestSingleCommandWithLineBreak()
        {
            var script = ParseAndLog("my-action1\r\n");

            Assert.AreEqual(1, script.Operations.Count);
        }

        [Test]
        public void TestSingleDirective()
        {
            var script = ParseAndLog("#MyDirective");

            Assert.AreEqual(1, script.Directives.Count);
            Assert.AreEqual("MyDirective", script.Directives[0].Text);
        }

        [Test]
        public void TestSingleDirectiveWithArguments()
        {
            var script = ParseAndLog("#MyDirective \"1\", \"2\"");

            Assert.AreEqual(1, script.Directives.Count);
            Assert.AreEqual("MyDirective", script.Directives[0].Text);
            Assert.AreEqual(2, script.Directives[0].Arguments.Count);
            Assert.AreEqual("1", script.Directives[0].Arguments[0]);
            Assert.AreEqual("2", script.Directives[0].Arguments[1]);
        }

        [Test]
        public void TestDirectivesWithinOtherLines()
        {
            var script = ParseAndLog("my-action1\r\n" +
                                     "#MyDirective\r\n" +
                                     "my-action2\r\n" +
                                     "#MyOtherDirective\r\n");

            Assert.AreEqual(2, script.Directives.Count);
            Assert.AreEqual("MyDirective", script.Directives[0].Text);
            Assert.AreEqual("MyOtherDirective", script.Directives[1].Text);
        }

        [Test]
        public void TestSingleComment()
        {
            var script = ParseAndLog("// This is a comment");

            Assert.AreEqual(1, script.Comments.Count);
            Assert.AreEqual(" This is a comment", script.Comments[0]);
        }

        [Test]
        public void TestCommentsWithinOtherLines()
        {
            var script = ParseAndLog("my-action1\r\n" +
                                     "// This is a comment\r\n" +
                                     "my-action2\r\n" +
                                     "// another comment\r\n");

            Assert.AreEqual(2, script.Comments.Count);
            Assert.AreEqual(" This is a comment", script.Comments[0]);
            Assert.AreEqual(" another comment", script.Comments[1]);
        }

        [Test]
        public void TestCommentsDirectivesAndCommandsCanAppearInAnyOrder()
        {
            var script1 = ParseAndLog("// comment\r\n" + "my-action1 \r\n" + "#MyDirective\r\n");
            Assert.AreEqual(1, script1.Comments.Count());
            Assert.AreEqual(1, script1.Directives.Count());
            Assert.AreEqual(1, script1.Operations.Count());

            var script2 = ParseAndLog("my-action1 \r\n" + "// comment\r\n" + "#MyDirective\r\n");
            Assert.AreEqual(1, script2.Comments.Count());
            Assert.AreEqual(1, script2.Directives.Count());
            Assert.AreEqual(1, script2.Operations.Count());

            var script3 = ParseAndLog("#MyDirective\r\n" + "my-action1 \r\n" + "// comment\r\n");
            Assert.AreEqual(1, script3.Comments.Count());
            Assert.AreEqual(1, script3.Directives.Count());
            Assert.AreEqual(1, script3.Operations.Count());
        }

        [Test]
        public void TestWithNewLineAtStart()
        {
            var text = @"
ping ""localhost""";

            var script = ParseAndLog(text);

            Assert.AreEqual(1, script.Operations.Count());
        }

        [Test]
        public void TestWithSingleSpaceOnLine()
        {
            var text = @"ping ""localhost""
 
ping ""myServer""";

            var script = ParseAndLog(text);

            Assert.AreEqual(2, script.Operations.Count());
        }

        [Test]
        [ExpectedException(typeof(InvalidOperationException), ExpectedMessage = "Script contains syntax errors")]
        public void TestBadlyFormedThrowsException()
        {
            ParseAndLog("(");
        }

        [Test]
        public void TestSingleEmptyGroup()
        {
            var script = ParseAndLog("{}");

            Assert.AreEqual(0, script.Operations.Count);
            Assert.AreEqual(0, script.Comments.Count);
            Assert.AreEqual(0, script.Directives.Count);
        }


        [Test]
        public void TestSingleGroup()
        {
            const string source = "{\r\n" +
                                  "  #my-directive\r\n" +
                                  "  my-action1\r\n" +
                                  "  // my comment\r\n" +
                                  "  my-action2\r\n" +
                                  "}";

            var script = ParseAndLog(source);
            Assert.AreEqual(1, script.ChildGroups.Count);
            Assert.AreEqual(0, script.Operations.Count);
            Assert.AreEqual(0, script.Comments.Count);
            Assert.AreEqual(0, script.Directives.Count);

            var childGroup1 = script.ChildGroups[0];
            Assert.AreEqual(0, childGroup1.ChildGroups.Count);
            Assert.AreEqual(2, childGroup1.Operations.Count);
            Assert.AreEqual(1, childGroup1.Comments.Count);
            Assert.AreEqual(1, childGroup1.Directives.Count);

        }

        [Test]
        public void TestMultipleGroupsAtTopLevel()
        {
            const string source = "{\r\n" +
                                  "  my-action1\r\n" +
                                  "}\r\n" +
                                  "{\r\n" +
                                  "  my-action2\r\n" +
                                  "  my-action3\r\n" +
                                  "}";
            var script = ParseAndLog(source);

            Assert.AreEqual(2, script.ChildGroups.Count);
            Assert.AreEqual(0, script.Operations.Count);
            
            var childGroup1 = script.ChildGroups[0];
            Assert.AreEqual(0, childGroup1.ChildGroups.Count);
            Assert.AreEqual(1, childGroup1.Operations.Count);

            var childGroup2 = script.ChildGroups[1];
            Assert.AreEqual(0, childGroup2.ChildGroups.Count);
            Assert.AreEqual(2, childGroup2.Operations.Count);
        }

        [Test]
        public void TestNestedGroupsAtTopLevel()
        {
            const string source = "{\r\n" +
                                  "  {\r\n" +
                                  "    my-action1\r\n" +
                                  "    my-action2\r\n" +
                                  "  }\r\n" +
                                  "  {\r\n" +
                                  "    my-action3\r\n" +
                                  "    my-action4\r\n" +
                                  "    my-action5\r\n" +
                                  "  }\r\n" +
                                  "}\r\n" +
                                  "{\r\n" +
                                  "  my-action6\r\n" +
                                  "}";

            var script = ParseAndLog(source);

            Assert.AreEqual(2, script.ChildGroups.Count);

            var childGroup1 = script.ChildGroups[0];
            Assert.AreEqual(2, childGroup1.ChildGroups.Count);

            var childGroup1A = childGroup1.ChildGroups[0];
            Assert.AreEqual(0, childGroup1A.ChildGroups.Count);
            Assert.AreEqual(2, childGroup1A.Operations.Count);

            var childGroup1B = childGroup1.ChildGroups[1];
            Assert.AreEqual(0, childGroup1B.ChildGroups.Count);
            Assert.AreEqual(3, childGroup1B.Operations.Count);

            var childGroup2 = script.ChildGroups[1];
            Assert.AreEqual(0, childGroup2.ChildGroups.Count);
            Assert.AreEqual(1, childGroup2.Operations.Count);
        }

        [Test]
        public void TestNestedGroupsMixedWithContent()
        {
            const string source = "{\r\n" +
                                  "  my-action1\r\n" +
                                  "  {\r\n" +
                                  "    my-action2\r\n" +
                                  "    my-action3\r\n" +
                                  "  }\r\n" +
                                  "}\r\n" +
                                  "{\r\n" +
                                  "  my-action4\r\n" +
                                  "  my-action5\r\n" +
                                  "  my-action6\r\n" +
                                  "}";

            var script = ParseAndLog(source);
            Assert.AreEqual(2, script.ChildGroups.Count);

            var childGroup1 = script.ChildGroups[0];
            Assert.AreEqual(2, childGroup1.ChildGroups.Count);

            var childGroup1A = childGroup1.ChildGroups[0];
            Assert.AreEqual(0, childGroup1A.ChildGroups.Count);
            Assert.AreEqual(2, childGroup1A.Operations.Count);

            var childGroup1B = childGroup1.ChildGroups[1];
            Assert.AreEqual(0, childGroup1B.ChildGroups.Count);
            Assert.AreEqual(1, childGroup1B.Operations.Count);

            var childGroup2 = script.ChildGroups[1];
            Assert.AreEqual(0, childGroup2.ChildGroups.Count);
            Assert.AreEqual(3, childGroup2.Operations.Count);
        }

        private void AssertAction(ActionDefinition action, string expectedName, params string[] expectedArgs)
        {
            Assert.AreEqual(expectedName, action.Name, "Expected name");
            Assert.AreEqual(expectedArgs.Count(), action.Arguments.Count(), "Expected argument count");

            for (int i = 0; i < expectedArgs.Length; i++)
            {
                Assert.AreEqual(expectedArgs[i], action.Arguments[i]);
            }
        }

        private static ScriptDefinition ParseAndLog(string scriptSource)
        {
            Trace.Write("Syntax tree:\r\n" + scriptSource.GetSyntaxTree().Describe() + "\r\n\r\n");

            var script = scriptSource.Parse();
            Trace.Write(script.ToString());
            return script;
        }
    }

    
}
